package org.hilo.boot.app.web;

import java.util.HashSet;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.hilo.boot.core.OUT;
import org.hilo.boot.core.RefMsg;
import org.hilo.boot.core.UT;
import org.hilo.boot.core.exception.AppException;
import org.hilo.boot.core.util.HiloStackTraceFilter;
import org.jretty.apibase.Result;
import org.jretty.util.JrettyCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.NestedServletException;

/**
 * 异常集中处理器
 * 
 * @author zollty
 */
abstract public class GlobalExceptionResolver implements HandlerExceptionResolver, Ordered {
    private Logger logger = LoggerFactory.getLogger(getClass());
    // private static final StackTraceElement[] EMPTY_STACK = new StackTraceElement[0];
    // 时间越长，日志越少，注意有时候无日志或者日志条数不对，就是因为这个原因。这个不能代替限流，建议加上限流。
    private JrettyCache cache = new JrettyCache(128, 3000);
    /**
     * Standard Spring exceptions
     */
    private static Set<Class<?>> springExceptions = new HashSet<>();
    static {
        springExceptions.add(org.springframework.beans.ConversionNotSupportedException.class);
        springExceptions.add(org.springframework.beans.TypeMismatchException.class);
        springExceptions.add(org.springframework.http.converter.HttpMessageNotReadableException.class);
        springExceptions.add(org.springframework.http.converter.HttpMessageNotWritableException.class);
        springExceptions.add(org.springframework.validation.BindException.class);
        springExceptions.add(org.springframework.web.HttpMediaTypeNotAcceptableException.class);
        springExceptions.add(org.springframework.web.HttpMediaTypeNotSupportedException.class);
        springExceptions.add(org.springframework.web.HttpRequestMethodNotSupportedException.class);
        springExceptions.add(org.springframework.web.bind.MethodArgumentNotValidException.class);
        springExceptions.add(org.springframework.web.bind.MissingPathVariableException.class);
        springExceptions.add(org.springframework.web.bind.MissingServletRequestParameterException.class);
        springExceptions.add(org.springframework.web.bind.ServletRequestBindingException.class);
        springExceptions.add(org.springframework.web.context.request.async.AsyncRequestTimeoutException.class);
        springExceptions.add(org.springframework.web.multipart.support.MissingServletRequestPartException.class);
        springExceptions.add(org.springframework.web.servlet.NoHandlerFoundException.class);
        springExceptions.add(NestedServletException.class);
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
    
    @Override
    public ModelAndView resolveException(HttpServletRequest request, 
            HttpServletResponse response, Object handler, Exception ex) {
        Result<?> r = null;
        int unknownErrorStatus = 200;
        if (ex instanceof AppException) {
            // I18nException，不必记录日志
            r = Result.fail(((AppException) ex).getErrorCode(), 
                    ((AppException) ex).getMessage(request.getLocale()));
            // ex.setStackTrace(EMPTY_STACK); // 清空堆栈信息
        } else {
            unknownErrorStatus = 500; // 未知异常
            // 控制日志打印频率
            String key = UT.Excp.getExceptionSign(ex);
            r = cache.getData(key);
            if (r != null) { // 已缓存日志
                r.setTimestamp(System.currentTimeMillis());
            } else { // 新日志，若为Spring异常，则此处直接记录，否则调用handleMoreException()继续。
                for (Class<?> cls : springExceptions) {
                    if (cls.isInstance(ex)) {
                        r = Result.fail(new RefMsg().getCode(), ex.getClass().getSimpleName());
                        autoLogError(request, r, ex, key);
                        break;
                    }
                }
                
                r = r == null ? handleMoreException(request, ex) : r;
            }
            
            if (r == null) { // 如果handleMoreException()没有处理，此处直接记录
                r = Result.fail(new RefMsg().getCode(), ex.getClass().getSimpleName());
                autoLogError(request, r, ex, key);
            }
        }

        if(ex instanceof NestedServletException) { // 此类型错误没办法处理了
            return new ModelAndView("internal error!", HttpStatus.INTERNAL_SERVER_ERROR);
        }
        
        Result<?> nr = beforeErrorViewWrite(request, response, handler, ex, r);

        return new ModelAndView(new SimpleJsonView(UT.Json.toJSONString(nr == null ? r : nr), unknownErrorStatus));
    }
    
    
    /**
     * 记录异常日志 如果最近1秒内有这个相同的异常，则不再记录。1秒钟后，重新计时。
     */
    protected void autoLogError(HttpServletRequest request, Result<?> r, 
            Throwable ex, String key) {
        if (cache.putIfAbsent(key, r)) {
            String uri = request.getRequestURI();
            
            logger.error(r.getCode() + ": execution at: " + uri + "\n"
                    + UT.Excp.getStackTraceStr(HiloStackTraceFilter.me(), ex));
            
            OUT.log("ERR: " + r.getCode() + ": execution at: " + uri + "\n"
                    + UT.Excp.getStackTraceStr(HiloStackTraceFilter.me(), ex));
            
        } else {
            OUT.log(key + "-KEY已存在，未过期， CACHE SIZE=" + cache.size());
        }
    }
    
    abstract public Result<?> handleMoreException(HttpServletRequest request, Exception ex);

    abstract public Result<?> beforeErrorViewWrite(HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception ex, Result<?> ret);

}
